PROGRAMACIÓN
FUNCIONAL

¿Se puede liberar la programación del estilo von Neumann? (John Backus)

“Lo que fue nuevo y esencial para toda la matemática fue la concepción enteramente general de función” (Dedekind)



El Paradigma Funcional

El paradigma funcional de programación se basa en utilizar un solo concepto (o primitiva conceptual): la función, en el sentido matemático del término. Nació como reacción al paradigma imperativo. John Backus ha sido uno de los máximos impulsores de la semántica funcional de programación, criticando el concepto de variables y el de sentencias de asignación, afirmando que son la raíz de muchos males y una “maldición” transmitida por el modelo von Neumann [Backus, 1978].


Características del paradigma funcional
Limitaciones del paradigma funcional

La aplicación exclusiva del paradigma funcional conduce a graves dificultades expresivas:
Especificación en MENTAL

Definición de función

Una función f es una expresión dependiente de uno o varios parámetros que, al ser evaluada con los mismos argumentos produce siempre el mismo resultado.
Formas de especificar y evaluar funciones

Existen muchas formas de especificar y evaluar funciones: de forma directa (sin nombre), con nombre, con y sin parámetros formales, con o sin evaluación diferida, con sustituciones absolutas o relativas, con evaluación parcial o total, etc.
Ejemplos de funciones con parámetros

En los ejemplos siguientes, por su mayor claridad, vamos a utilizar expresiones genéricas parametrizadas para definir funciones.
  1. Serie de Fibonacci.
    Produce una secuencia con los n primeros números de la serie de Fibonacci.

    ⟨( fibo(n) = ( (n=1 → ¡1 )
    (n=2 → ¡(1 1))
    (s = fibo(n−1))
    ¡(s↓ (s\(s#) + s\(s#−1)))
    )!
    )⟩

    fibo(1) // ev. 1
    fibo(2) // ev. (1 1)
    fibo(3) // ev. (1 1 2)
    fibo(8) // ev. (1 1 2 3 5 8 13 21)


  2. Conversión de una secuencia x de dígitos binarios a un número decimal.
    El procedimiento consiste en multiplicar el primer dígito por 2 y sumarle el segundo, multiplicar el resultado por 2 y sumarle el tercero, etc., hasta llegar a sumar el último. Por ejemplo, 1011 = ((1*2 + 0)*2 + 1)*2 + 1.

    ⟨( bindec(x) = ( (r = 0)
    [(r = (r*2 + x\[1…x#]))]
    ¡r )! )⟩

    bindec(1011) // ev. 11


  3. Conversión de un número entero positivo n en una secuencia de dígitos binarios.
    El procedimiento consiste en dividir el número n entre 2, el cociente se vuelve a dividir entre 2, etc. hasta que el cociente sea 1. Entonces se toma el último cociente y todos los restos en orden inverso. Ejemplo para el número 19:

    Oper.CocienteRestoResultado
    19÷291←10011
    9÷241←
    4÷220←
    2÷21←0←

    ⟨( decbin(n) = ( (s = ()) // secuencia resultado inicial

    (m = n)

    ( (c = m÷2)) // cociente

    (c=1 → ¡(c s↓))

    (r = resto(m 2)) // resto

    // incluir resto al comienzo de la secuencia

    (s = (r s↓))

    (m = c) )★ )! )⟩

    decbin(19) // ev. (1 0 0 1 1)


  4. Función que devuelve ”S” si n es primo y ”N” en caso contrario.

    ⟨( primo(n) = ( n≤3 → ¡"S")
    ((resto(n 2) = 0) → ¡"N")
    [ (i = [(3 … nv2)])
    ((resto(n i) = 0) → ¡"N") ]
    ¡"S" )! )⟩


    Siendo

    ⟨( resto(n1 n2) = n1 – (n1÷n2)*n2 )⟩

    el resto de la división entre dos números enteros. La expresión nv2 es la parte entera de √n.

Funciones recursivas

Son funciones que se llaman a sí mismas. El uso de la recursividad simplifica notablemente la definición de funciones.

Un ejemplo representativo es el de los recorridos en amplitud y profundidad de un árbol. Si tenemos el árbol


es decir,

( árbol = ( (a b) (a c) (a d)
(b e) (b f) (c g) (d h) (d i) ) )


o bien, usando la distribución,

( árbol = ( [(a [b c d])]
[(b [e f])] (c g) [(d [h i])] ) )


en donde (x y) representa la rama del árbol que va del nodo x al nodo y.

Recorrido del árbol en amplitud (nodo es el nodo raíz):

⟨( amplitud(árbol nodo) = (
(s = (⟨ (x ← (nodo x)∈árbol ⟩) // secuencia de nodos hijos
(¡( nodo ) ← s=() →'
¡( nodo s↓ [amplitud(árbol [s↓])↓] )
)! )⟩

amplitud(árbol a) // ev. (a b c d e f g h i)
amplitud(árbol b) // ev. (b e f)
amplitud(árbol e) // ev. ( e )


Recorrido del árbol en profundidad (nodo es el nodo raíz):

⟨( profundidad(árbol nodo) = (
(s = (⟨ (x ← (nodo x)∈árbol ⟩) // secuencia de nodos hijos
(¡( nodo ) ← s=() →'
¡( nodo s\1 [profundidad(árbol ([s ∪’ s\1])↓] )
)! )⟩

profundidad(árbol a) // ev. (a b e f c g d h i)
profundidad(árbol b) // ev. (b e f)
profundidad(árbol e) // ev. ( e )



Tablas

Una tabla es un caso particular de función en la que hay una correspondencia explícita y directa entre cada argumento y su resultado.

Ejemplos:
  1. Función y definida sobre un argumento x:

    xy
    12
    27
    35
    resto8

    ⟨( y(x) = (2 ← (x=1) →'
    (7 ← (x=2) →'
    (5 ← (x=3) →’ 8))) )⟩
    y(2) // ev. 7
    y(4) // ev. 8
    y("abc") // ev. 8


  2. Función z de dos argumentos (x, y) en donde α indica “cualquier expresión”:

    xyz
    xyz
    00a
    01b
    α0c
    1αd

    ⟨( z(x y) = ((a ← (x=0 → y=0) →'
    ((b ← (x=0 → y=1) →'
    ((c ← y=0) →’
    (d ← x=1)))) )⟩
    z(3 0) // ev. c
    z(1 3) // ev. d
    z(3 3) // ev. θ (función no definida para los argumentos)


  3. Función f de dos argumentos (x, y) que produce una secuencia de dos variables (u, v) con sus valores:

    xyuv
    aa27
    ba34
    cb16
    db59

    ⟨( f(x y) = ((x=a → y=a → ¡((u = 2) (v = 7)))
    (x=b → y=a → ¡((u = 3) (v = 4)))
    (x=c → y=b → ¡((u = 1) (v = 6)))
    (x=d → y=b → ¡((u = 5) (v = 9))) )⟩

    f(c b) // ev. (u=1 v=6)
    f(0 0) // ev. θ (función no definida para los argumentos)

Funciones que devuelven funciones

Son funciones que retornan como resultado otra función. Ejemplos:
  1. ⟨( f(n1 n2) = ⟨( g(n) = (n1 n2n) )⟩ )⟩
    f(3 7) // ev. ⟨( g(n) = (3 7 … n) )⟩


  2. ⟨( f(n1 n2) = ( ⟨( g(n) = (n1n2n) )⟩ )⟩ ← n1>n2 →'
    ⟨( g(n) = (n1+n2n) )⟩ ) )⟩
    f(3 7) // ev. ⟨( g(n) = (10 … n) )⟩
    f(7 3) // ev. ⟨( g(n) = (4 … n) )⟩

Funciones como argumentos de funciones

Una función se pueden pasar como argumento de otra función. Ejemplo:

⟨( f(g n1 n2) = ⟨( ( [g([(n1n2)!])] )/g ) )⟩ )⟩
f(⟨( g(n) = 2*n+1 )⟩ 1 4)
// ev. (g(1) g(2) g(3) g(4))/ ⟨( g(n) = 2*n+1 )⟩
// ev. (3 5 7 9)



Funciones de orden superior

Son funciones que tienen como funciones como argumentos y que retornan como resultado otra función. En general, una expresión paramétrica se puede pasar como argumento de una función. Ejemplos:
  1. ⟨( f(u v) = ⟨( h(x y) = (u + v + 7) )⟩ )⟩
    f(x*x y*y) // ev. ⟨( h(x y) = (x*x + y*y + 7) )⟩


  2. ⟨( f(g n) = ⟨( h(x) = (ng(x))/g )⟩ )⟩
    f(⟨( g(x) = 2*x+1 )⟩ 4)
    // ev. (⟨( h(x) = (4★g(x))/⟨( g(x) = 2*x+1 )⟩ )⟩ )⟩
    // ev. (⟨( h(x) = (4★(2*x+1) )⟩

Aplicación de una función a una secuencia de argumentos

En MENTAL es fácil realizarlo mediante la primitiva “Distribución”.

Ejemplo con un parámetro:

⟨( triple(x) = 3×x )⟩
( [triple([1 2 3 4])] ) // ev. ( 3 6 9 12 )
( [triple[( 2…5 )])] ) // ev. ( 6 9 12 15 )


Ejemplo con dos parámetros:

⟨( f(x y) = (x+y x*y) )⟩
( [f[(1 2) (3 4) (5 6)]] ) // eq. ( f(1 2) f(3 4) f(5 6) )
// ev. ( (3 2) (7 12) (11 30) )


También se puede hacer mediante sustitución relativa. Para los dos ejemplos anteriores:

(1 2 3 4)/⟨( triple(n) = 3*n )⟩ // ev. ( 3 6 9 12 )

((1 2) (3 4) (5 6))/⟨( f(n1 n2) = (n1+n2 n1*n2) )⟩ // ev. ( (3 2) (7 12) (11 30) )


Obsérvese que los parámetros de la función tienen que indicar el tipo de elementos a los que se aplican (números enteros, en este caso).


Aplicación recursiva de una función

La aplicación recursiva (n veces) de una función f(x), con argumento a, es decir, por ejemplo, con n=4: f(f(f(f(a)))) se puede especificar mediante (f★n a)Δ siendo Δ el operador de agrupación binaria jerárquica, en este caso a la derecha. Recordemos que, por ejemplo,

(f f f a)Δ = f(f(f a))

Si el nombre de la función es fijo y el argumento a también, se podría definir la función de un parámetro:

⟨( z(n) = (f★n a)Δ )⟩

Ejemplo:

⟨( f(x y) = (x+y x*y) )⟩
⟨( z(n) = (f★n (a b))Δ )⟩
z(1) // ev. (f (a b)) eq. f(a b) ev. (a+b a*b)
z(2) // ev. (f (f (a b))) eq. f(f(a b)) // ev. ((a+b)+(a*b) (a+b)*(a*b))


Si el argumento x fuera variable, también podríamos definir la función de dos parámetros:

⟨( z(x n) = (f★n x)Δ )⟩

Ejemplo:

⟨( f(x y) = (x+y x*y) )⟩
z((a b) 1) // ev. (f (a b)) eq. f(a b) ev. (a+b a*b)
z((a b) 2) // ev. (f (f (a b))) eq. f(f(a b)) // ev. ((a+b)+(a*b) (a+b)*(a*b))


La máxima generalidad se logra cuando la propia función es también un parámetro:

⟨( z(f x n) = ((fn x)Δ)/f )⟩

Ejemplo:

z(⟨( f(x y) = (x+y x*y) )⟩ (a b) 1) // ev. (f (a b))/⟨( f(x y) = (x+y x*y) )⟩ ev. (a+b a*b)

z(⟨( f(x y) = (x+y x*y) )⟩ (a b) 2) // ev. (f (f (a b)))/⟨( f(x y) = (x+y x*y) )⟩ ev. f(f(a b)) ev. ((a+b)+(a*b) (a+b)*(a*b))



Evaluación parcial de funciones

Se trata de suministrar menos argumentos que parámetros tiene definidos la función. Esto permite definir funciones particulares, instanciadas a partir de funciones más generales. Ejemplo:

⟨( f(x y) = (x+y x*y) )⟩
f(5 y) // ev. (5+y 5*y)
⟨( g(y) = f(5 y) )⟩ // ev. ⟨( g(y) = (5+y 5*y) )⟩
g(7) // ev. (12 35)


En MENTAL, el mecanismo de evaluación parcial es genérico, es decir, es aplicable a todo tipo de expresiones [ver Lenguaje MENTAL – Técnicas – Evaluación parcial].



Bibliografía